Skip to main content

Jetpack Compose Integration

Implementing the Biometric Capture Session in a Jetpack Compose-based UI

This example shows how to integrate the Biometric Capture Session to your project. The Biometric Capture Session extracts a frames collection from the camera preview.

Create a compose activity.

//ComposeBiometric.kt

class ComposeBiometric : ComponentActivity() , MBCaptureSessionServiceDelegate {


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()

val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)

setContent {
//Starting screen here
}
}


override fun onCaptureFinished(result: MBCaptureSessionResult?) {

}

override fun onCaptureSessionStatusChanged(captureStatus: MBCaptureSessionStatus) {

}

override fun onWaitingToCapture(faceStatus: MBFaceStatus) {

}

override fun onFailure(errorEnum: MBCaptureSessionError) {

}

override fun onCaptureStarted() {

}
}

Create an instance of MBCaptureSessionOptions builder. It contains the default options for performing the capture session. Those default options can be redefined. In this case the capture process is set to automatic.

 val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()

Creates an instance of MBCaptureSessionInstance. This is the entry point configuration for the capture process.

 val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)

The function onCaptureFinished gets a result of the capture process if it is successfully finished. result contains a list of nullable Bitmaps.

 override fun onCaptureFinished(result: MBCaptureSessionResult?) {

}

The function onCaptureSessionStatusChanged is executed every time the capture process changes its state. MBCaptureSessionStatus describes those states.

  override fun onCaptureSessionStatusChanged(captureStatus: MBCaptureSessionStatus) {

}

The function onWaitingToCapture is executed every time the face state changes. MBFaceStatus describes the state of the face in the camera preview.

override fun onWaitingToCapture(faceStatus: MBFaceStatus) {

}

The function onFailure is executed when the capture process fails. MBCaptureSessionError tells the type of error that occurred.

override fun onFailure(errorEnum: MBCaptureSessionError) {

}

The method onCaptureStarted is executed every time a face is valid and starts capturing.

  override fun onCaptureStarted() {

}

Add the following dependencies in the build gradle app.

Enables LiveData for Jetpack Compose.

    implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
def nav_version = "2.5.2"

Enables navigation in Jetpack Compose.

    implementation "androidx.navigation:navigation-compose:$nav_version"

Coil helps to display a bitmap.

    //Coil
implementation("io.coil-kt:coil-compose:1.4.0")

Implement a capture screen

Create the layout that contains the capture session view.

//container.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/text_color_disabled"
android:id="@+id/ll_view_container">

</LinearLayout>

Implement the composable function that defines the capture screen/camera preview. MBCaptureSessionService instance of the entry point configuration for the capture process. MBFaceStatus gets the face status every time it changes in the camera preview. MBCaptureSessionStatus gets a capture status every time it changes.

//CaptureScreen.kt

@SuppressLint("UnsafeOptInUsageError")
@Composable
fun CaptureScreen(
captureSessionService: MBCaptureSessionService,
faceStatus: MBFaceStatus,captureStatus: MBCaptureSessionStatus
) {
Surface(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { context ->
View.inflate(context, R.layout.container, null).apply {
val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)

if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())
captureSessionService.startCamera()
}
},
modifier = Modifier.fillMaxSize(),
)

if (!captureSessionService.getAutomaticCapture()) {
Box(modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.padding(bottom = 100.dp),
contentAlignment = Alignment.BottomCenter) {
Button(modifier = Modifier
.width(150.dp)
.height(50.dp),
onClick = {
captureSessionService.startCaptureSession()
}) {
Text(text = "Start Capture")
}
}
}
}
}

Inflates camera preview based UI into a Jetpack Compose UI using AndroidView with that propose

 AndroidView(
factory = { context ->
View.inflate(context, R.layout.container, null).apply {
val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)

if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())

captureSessionService.startCamera()
}
},
modifier = Modifier.fillMaxSize(),
)

Inflates the layout container view.

View.inflate(context, R.layout.container, null)

Create a layout container instance to be able to add a child view to it.

val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)

Add the capture session view to the container ViewContainer.

if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())

Start the camera if permission is granted.

captureSessionService.startCamera()

Enable capture button if capture is manual or disable it if the capture is automatic.

     if (!captureSessionService.getAutomaticCapture()) {
Box(modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.padding(bottom = 100.dp),
contentAlignment = Alignment.BottomCenter) {
Button(modifier = Modifier
.width(150.dp)
.height(50.dp),
onClick = {
captureSessionService.startCaptureSession()
}) {
Text(text = "Start Capture")
}
}
}

Starts manual capture session if this is set to manual.

onClick = {
captureSessionService.startCaptureSession()
}

Implement a screen to display the collection of frames.

The composable ImagesScreen function displays the collection of frames in a LazyVerticalGrid. The function gets a list of nullable bitmaps as parameter.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ImagesScreen(frames: List<Bitmap?>) {
if (frames.isNotEmpty()) {
Surface(modifier = Modifier
.fillMaxSize()
.padding(3.dp),
color = Color.DarkGray) {

if (frames.isNotEmpty() && !frames.contains(null)) {
LazyVerticalGrid(
cells = GridCells.Fixed(2),
content ={
items(frames) { frame ->
Box(modifier = Modifier
.width(200.dp)
.height(300.dp)
.padding(3.dp),
contentAlignment = Alignment.Center){
Image(painter = rememberImagePainter(data = frame),"Image")
}
}
} )
}

}
}
}

ScreensRoute lists the screens contained in the example app.

sealed class ScreensRoute(val route: String) {
object CaptureScreen: ScreensRoute("main_screen")
object ImagesScreen: ScreensRoute("image_screen")
}

CaptureSessionViewModel updates and observes the different states of the capture process, MBFaceStatus and MBCaptureSessionStatus by using Livedata.

class CaptureSessionViewModel: ViewModel() {
private val _faceStatus: MutableLiveData<MBFaceStatus> = MutableLiveData(MBFaceStatus.NOT_FOUND)
val faceStatus: LiveData<MBFaceStatus> = _faceStatus

fun onFaceStatusChange(faceStatus: MBFaceStatus) {
_faceStatus.value = faceStatus
}

private val _captureStatus: MutableLiveData<MBCaptureSessionStatus> = MutableLiveData(MBCaptureSessionStatus.NOT_READY_TO_CAPTURE)
val captureStatus: LiveData<MBCaptureSessionStatus> = _captureStatus

fun setCaptureSessionStatus(status: MBCaptureSessionStatus) {
_captureStatus.value = status
}
}

FrameCollectionViewModel updates and observes the collection of frames we get within the capture session result by using Livedata.

class FrameCollectionViewModel : ViewModel(){

private val mutableFrameCollection = MutableLiveData<List<Bitmap?>>()
var frameCollection: LiveData<List<Bitmap?>> = mutableFrameCollection

fun setFrameCollection(frames: ArrayList<Bitmap?>) {
mutableFrameCollection.value = frames
}
}

The composable function CaptureNavigation controls the navigation between screens. Gets as parameters an instance of MBCaptureSessionService entry point configuration for the capture process, an instance of NavControllerCallback that provide a navigation controller instance to ComposeBiometric, an instance of CaptureSessionViewModel that get every change in the capture session status, and finally an instance of FrameCollectionViewModel

@Composable
fun CaptureNavigation(
captureService: MBCaptureSessionService,
provider: NavControllerCallback,
captureSessionViewModel: CaptureSessionViewModel = viewModel(),
frameCollectionViewModel: FrameCollectionViewModel = viewModel()
){
val navController = rememberNavController()
provider.provideController(navController)
val faceStatus: MBFaceStatus by captureSessionViewModel.faceStatus.observeAsState(MBFaceStatus.NOT_FOUND)
val frameCollection: List<Bitmap?> by frameCollectionViewModel.frameCollection.observeAsState(listOf())
val captureStatus: MBCaptureSessionStatus by captureSessionViewModel.captureStatus.observeAsState(MBCaptureSessionStatus.NOT_READY_TO_CAPTURE)

NavHost(navController = navController, startDestination = ScreensRoute.CaptureScreen.route) {
composable(ScreensRoute.CaptureScreen.route) {
CaptureScreen(captureSessionService = captureService, faceStatus = faceStatus, captureStatus = captureStatus)
}
composable(ScreensRoute.ImagesScreen.route) {
ImagesScreen(frames = frameCollection)
}
}
}

The NavController is the central API for the Navigation component. It is stateful and keeps track of the back stack of composables that make up the screens in your app and the state of each screen. Create a NavController by using the rememberNavController() method in the composable:

 val navController = rememberNavController()

Execute provideNavController, this function provides an instance of navController to BiometricCompose activity.

provider.provideController(navController)

Gets the instance of MBFaceStatus, MBCaptureSessionStatus and frameCollection is the collection of frames. We observe in ComposeBiometric activity.

    val faceStatus: MBFaceStatus by captureSessionViewModel.faceStatus.observeAsState(MBFaceStatus.NOT_FOUND)
val frameCollection: List<Bitmap?> by frameCollectionViewModel.frameCollection.observeAsState(listOf())
val captureStatus: MBCaptureSessionStatus by captureSessionViewModel.captureStatus.observeAsState(MBCaptureSessionStatus.NOT_READY_TO_CAPTURE)

Implement the NavHost function. This Provides in place in the Compose hierarchy for self contained navigation to occur. The CaptureScreen function gets as parameters captureService that is the entry point configuration for the capture process, faceStatus is the status of the face every time it changes and captureStatus the status of the capture process every time it changes. The ImageScreen function displays the collection of frames and gets the frame frameCollection as a parameter.

  NavHost(navController = navController, startDestination = ScreensRoute.CaptureScreen.route) {
composable(ScreensRoute.CaptureScreen.route) {
CaptureScreen(captureSessionService = captureService, faceStatus = faceStatus, captureStatus = captureStatus)
}
composable(ScreensRoute.ImagesScreen.route) {
ImagesScreen(frames = frameCollection)
}
}

Implement FrameCollectionViewModel , CaptureSessionViewModel viewModel and NavControllerCallback in ComposeBiometric activity, as well as execute CaptureNavigation and get the NavController instance.

class ComposeBiometric : ComponentActivity() , MBCaptureSessionServiceDelegate,NavControllerCallback {
private lateinit var navController: NavController
private val framesViewModel: FrameCollectionViewModel by viewModels()
private val captureSessionViewMode: CaptureSessionViewModel by viewModels()

@SuppressLint("UnsafeOptInUsageError")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)


val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()
val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)

setContent {
CaptureNavigation(
captureService = captureSessionService,
provider = this, captureSessionViewModel = captureSessionViewMode,
frameCollectionViewModel = framesViewModel
)
}
}

override fun onCaptureFinished(result: MBCaptureSessionResult?) {
this.runOnUiThread {
result?.let {
framesViewModel.setFrameCollection(it.frames)
navController.navigate(route = ScreensRoute.ImagesScreen.route)
}
}

}

override fun onCaptureSessionStatusChanged(captureStatus: MBCaptureSessionStatus) {
this.runOnUiThread {
captureSessionViewMode.setCaptureSessionStatus(status = captureStatus)
}
}

override fun onWaitingToCapture(faceStatus: MBFaceStatus) {
this.runOnUiThread {
captureSessionViewMode.onFaceStatusChange(faceStatus)
}
}

override fun onFailure(errorEnum: MBCaptureSessionError) {

}

override fun onCaptureStarted() {

}

override fun provideController(navController: NavController) {
this.navController = navController
}
}

Creates instances of NavController , FrameCollectionViewModel and CaptureSessionViewModel.

    private lateinit var navController: NavController
private val framesViewModel: FrameCollectionViewModel by viewModels()
private val captureSessionViewMode: CaptureSessionViewModel by viewModels()

Starts CaptureNavigation with captureSessionService , provider and the viewModes as parameter.

  setContent {
CaptureNavigation(
captureService = captureSessionService,
provider = this, captureSessionViewModel = captureSessionViewMode,
frameCollectionViewModel = framesViewModel
)
}

Initialize the NavController instance.

  override fun provideController(navController: NavController) {
this.navController = navController
}

Provides the collection of frames to the CaptureNavigation function, and navigates to `ImageScreen.

   override fun onCaptureFinished(result: MBCaptureSessionResult?) {
this.runOnUiThread {
result?.let {
framesViewModel.setFrameCollection(it.frames)
navController.navigate(route = ScreensRoute.ImagesScreen.route)
}
}

}

Updates the captureSessionViewMode passed as parameter to Capturenavigattion with a new status of the capture session.

  override fun onCaptureSessionStatusChanged(captureStatus: MBCaptureSessionStatus) {
this.runOnUiThread {
captureSessionViewMode.setCaptureSessionStatus(status = captureStatus)
}
}

Updates the captureSessionViewMode passed as parameter to Capturenavigattion with a new status of the in the camera preview.

    override fun onWaitingToCapture(faceStatus: MBFaceStatus) {
this.runOnUiThread {
captureSessionViewMode.onFaceStatusChange(faceStatus)
}
}